在大數據時代的 Web 應用中,搜尋和排序已成為網站的基本需求,它們確保用戶能在這浩瀚的資訊海洋中迅速找到方向。
透過這兩大功能,不僅提升了網站的使用效率,更增強了使用者的滿意度和體驗。
Day16 開始前分支:feat/day_15/enhance_blog_list_view_by_pagination
Day16 進度完成分支:feat/day_16/implement_search_function
在本篇文章中,我們將探討如何在 GraphQL 中進行條件查詢,包含了按照特定條件進行篩選和排序的功能。
開始實作前,我們先使用 Fake Online GraphQL API — GraphQLZero 示範如何撰寫條件查詢。
首先,我們先取得所有的使用者清單,以便確保後續的篩選結果是正確的。
為了達到這個目的,我們將撰寫名為 getUsers
的 GraphQL 查詢。
首先,我們來查看 GraphQLZero Play Ground 的 Docs。
(題外話,感謝 GraphQL 的規範,後端開發 API 時只要根據 Schema 加上簡單的註解,我們就能夠自動生成詳細的文件,大幅減少了人工撰寫文件的時間。)
在下圖中,我們逐步瀏覽,查閱 users Field 的巢狀屬性和其型別。
取得所有的使用者清單的 GraphQL 查詢:
query getUsers {
users {
data {
id
name
email
}
}
}
查詢結果
接著,讓我們加上條件搜尋,一樣先查看文件:
撰寫查詢搜尋包含關鍵字 Shanna
的特定使用者:
// 查詢
query getUsersBySearch(
$options: PageQueryOptions
) {
users(options: $options) {
data {
id
name
email
}
}
}
// 變數
{
"options": {
"search": {
"q": "Shanna"
}
}
}
查詢結果
預設的排序方式,大多數時候是由後端的資料來源或資料庫來決定的,或者是由後端開發者在實作 GraphQL 解析器(resolver)時明確定義某個預設排序方式(例如按照日期降冪排序)。
例如,如果 GraphQL 伺服器是連接到一個 PostgreSQL 資料庫,並且在查詢時沒有明確地指定排序條件,那麼返回的資料順序可能會是基於資料庫中的實際儲存順序。
在前端開發過程中,深入理解伺服器的預設排序行為或主動指定排序條件是至關重要的。這不僅確保我們能夠精準地掌握資料的呈現順序,還有助於我們更高效地整理和展示資料,從而提供更佳的用戶體驗。
反過來說,如果我們自作聰明,擅自認定伺服器的預設排序行為總是與我們的期望相吻合,那麼很可能就會遭遇意外的 Bug。
例如,我們可能期待最新日期的文章總是出現在最前面,但伺服器實際上可能是按照文章ID或其他標準來排序的,這將導致前端展示出現混亂,給使用者帶來困惑。
可以看到 API 提供了 sort
,其中 ASC
代表「升冪排序」,而 DESC
代表「降冪排序」。
修改先前的 getPostsPerPage
的變數:
{
"options": {
"paginate": {
"page": 1,
"limit": 3
},
"sort": {
"field": "id",
"order": "DESC"
}
}
}
查詢結果
考慮到篇幅的限制,我們先介紹基礎的部分。在基礎操作得心應手之後,如果 GraphQL API 伺服器提供更進階的支援,我們還可以進一步利用比較運算子來執行更多元的搜尋和排序策略。
例如,我們可以查詢評論數超過 99 的熱門文章,並結合日期的降冪排序,確保最新的文章始終位於前端展示的最前面。
讓我們開始實作!首先,我們需要了解文章搜尋功能的實作流程:
keyword
的查詢字符串 (query string)。
完整程式碼範例請參考:feat/day_16/implement_search_function
在 src/router/index.ts
新增 search-results
路由:
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// ...
{
path: '/search',
name: 'search-results',
component: () => import('@/views/BlogSearchView.vue'),
props: route => ({ keyword: route.query.keyword }),
},
// ...
],
})
這樣設定之後,當使用者訪問 /search?keyword=someKeyword
這個路由,BlogSearchView
Component 將會被渲染。在該元件內,我們可以直接使用 props.keyword
來獲取 someKeyword
,並基於此進行相對應的搜尋處理。
首先,新增 src/views/BlogSearchView.vue
,接著我們來分析這個元件需要擔任的職責:
src/views/BlogListView.vue
是相似的。在 src/views/BlogSearchView.vue
中,取得 props.keyword
並進行驗證
<script setup lang="ts">
// ...
const props = defineProps<{
keyword?: string
}>()
// 取得 props.keyword
const { keyword } = toRefs(props)
function sanitizeKeyword(inputKeyword: string | undefined): string {
if (!inputKeyword)
return ''
// 只允許英文、數字、底線、減號、空白
let sanitized = inputKeyword.replace(/[^a-zA-Z0-9\-_ ]/g, '')
// 避免 DoS 攻擊或其他可能的問題,限制長度 100
const maxLength = 100
if (sanitized.length > maxLength)
sanitized = sanitized.substring(0, maxLength)
return sanitized
}
const sanitizedKeyword = computed(() => {
return sanitizeKeyword(keyword?.value)
})
// ...
</script>
注意
:我們在此示範的驗證方式可能過於保守和基礎。儘管這種方法可能更加安全,但我們必須認識到安全性和便利性有時可能是相互衝突的。根據實際的產品需求,開發者會需要進行適當的調整。
我們透過簡單的 v-if
根據 sanitizedKeyword
去判斷要顯示何種提示框src/views/BlogSearchView.vue
<template>
<div
v-if="sanitizedKeyword"
class="flex items-center p-4 mb-4 text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400"
role="alert"
>
<!-- ... -->
</div>
<div v-else>
<!-- ... -->
<div>
<span class="font-medium">未輸入關鍵字</span>
</div>
<!-- ... -->
</div>
<!-- ... -->
</template>
src/views/BlogSearchView.vue
<script setup lang="ts">
// ...
const { result, loading, error } = useQuery(gqlGetPostsPerPage, () => ({
options: {
// 分頁
paginate: {
page: currentPage.value,
limit: limit.value,
},
// 搜尋 by sanitizedKeyword
search: {
q: sanitizedKeyword.value,
},
// 根據 id 降冪排序
sort: [{
field: 'id',
order: SortOrderEnum.Desc,
}],
},
}))
// ...
</script>
值得注意的是,我們使用了與 src/views/BlogListView.vue
相同的 GraphQL 文件 gqlGetPostsPerPage
,這展現了模組化重構的優點,我們只需要修改 GraphQL Variables 就能夠輕鬆獲得不同的查詢結果。
這部分可以直接重複使用 src/views/BlogListView.vue
同樣的邏輯。
使用 Vue 的 v-model
結合 Vue Router,讓我們能迅速地完成此功能!
首先,在 src/components/layouts/header/Header.vue
,我們設定 searchTerm
和定義 handleSearch
方法:
<script setup lang="ts">
// ...
const router = useRouter()
// 使用 ref 雙向綁定搜尋關鍵字
const searchTerm = ref('')
// 利用 Vue Router 的 push 方法來處理當搜尋表單被提交的行為
function handleSearch() {
if (searchTerm.value)
router.push({ name: 'search-results', query: { keyword: searchTerm.value } })
}
// ...
</script>
接著,我們將 searchTerm
使用 v-model
綁定到搜尋輸入框上,如下:src/components/layouts/header/Header.vue
<input
id="topbar-search"
v-model="searchTerm"
type="text"
name="email"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
>
這樣就完成囉!這就是使用 Vue 框架能夠達到的流暢且高效的開發體驗!
鍵入 temporibus
查看搜尋結果
與 Play Ground 的測試結果一致
在今日的文章中,我們透過實際應用 GraphQL 的多種條件查詢技巧,深入探討並實現了部落格的多元搜尋和排序功能。
然而,在實際開發中,我們常常需要更多細緻的策略和考量。例如,目前在 Header 與 Sidebar 中存在了重複的搜尋功能邏輯。而這正是 Vue Composition API 的 Composable 功能可以發揮的地方,讓我們能夠更有效地重構和管理重複的程式碼。
此外,我們在文章中也提到了 XSS 攻擊的風險。雖然現今的瀏覽器和框架已經提供了許多安全防護機制,但對開發者而言,理解這些潛在的風險及其背後的原理仍然十分重要。只有當我們明白哪些做法是不佳的,我們才能確保撰寫更為健全和安全的程式。
期待明天的教學,我們將探討 Vue 框架如何使用插值 (Interpolation) 進行文本轉譯 (Text Interpolation),以及哪些程式碼寫法容易導致安全風險。